package com.cynhard.service;

import com.alibaba.fastjson.JSON;
import com.cynhard.common.RedisKeyPrefixConst;
import com.cynhard.common.RedisUtil;
import com.cynhard.dao.ProductDao;
import com.cynhard.model.Product;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Service
public class ProductService {

    @Autowired
    private ProductDao productDao;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private Redisson redisson;

    public static final Integer PRODUCT_CACHE_TIMEOUT = 60 * 60 * 24;
    public static final String EMPTY_CACHE = "{}";
    public static final String LOCK_PRODUCT_HOT_CACHE_PREFIX = "lock:product:hot_cache:";
    public static final String LOCK_PRODUCT_UPDATE_PREFIX = "lock:product:update:";

    //这里个MAP属性是为了应对redis雪崩，伪代码 ，加了一级缓存 (map是jvm级别是有容量限制的，而且分布式微服务的话 一个web发生变化 还需要其他的web端识别到，还需要mq来订阅 ，同一发给mq，让mq来处理)
    //最后再往上优化 就是单独再用AOP来拦截一层，实时流计算分析来处理数据
    public static Map<String, Product> productMap = new ConcurrentHashMap<>();




    @Transactional
    public Product create(Product product) {
        Product productResult = productDao.create(product);      //从前端传入，往数据里面插入
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
                genProductCacheTimeout(), TimeUnit.SECONDS);    //调用redis 将数据存到缓存中. redisUtil重新封装了下，key 加前缀PROduct_cache + 商品id ,value 转成hash
        return productResult;
    }


    //简易版更新数据  ，也可以使用delete逻辑
    @Transactional
    public Product update01(Product product){
        Product productResult = productDao.update(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(),JSON.toJSONString(productResult),
        PRODUCT_CACHE_TIMEOUT,TimeUnit.SECONDS);              //最后这两个参数 增加超时时间 ，以应对90%的冷门商品

        return productResult;
    }

    //版本02 ，批量执行的时候，设置这一批key 同一时间的话，会造成缓存击穿（这一批key没了的话，就都没了）  ，解决：增加随机时间genProductCacheTimeout(),这里使用了就得在get的里面也修改好 ，不在同一时刻都失效
    @Transactional
    public Product update01(Product product){
        Product productResult = productDao.update(product);
        redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(),JSON.toJSONString(productResult),
                genProductCacheTimeout(),TimeUnit.SECONDS);              //最后这两个参数 增加超时时间 ，以应对90%的冷门商品

        return productResult;
    }

    //升级版
    @Transactional
    public Product update02(Product product) {
        Product productResult = null;
        RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        updateProductLock.lock();

        // 线上都是读多写少，比好价格后才下单，浏览的时间占大部分。 这里是又将锁分成了 读锁update的时候用(这个可以并行执行) 和写锁
//        RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
//        RLock writeLock = readWriteLock.writeLock();
//        writeLock.lock();
        try {
            productResult = productDao.update(product);
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
                    genProductCacheTimeout(), TimeUnit.SECONDS);

            //二级缓存
            productMap.put(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), product);
        } finally {
            updateProductLock.unlock();
//            writeLock.unlock();
        }

        return productResult;
    }


    //简易版 的reids中没数据， 从后端拿数据插入
    public Product get01(Long productId) {
        Product product = null;

        // RedisKeyPrefixConst.PRODUCT_CACHE的目的是为了保持前缀相同
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

        //从redis缓存中把商品拿出来
        String productStr = redisUtil.get(productCacheKey);
        if (!StringUtils.isEmpty(productStr)) {  // 判断字符串是不是为空
            product=JSON.parseObject(productStr,Product.class);         //转换成商品直接返回
            redisUtil.expire(productCacheKey,PRODUCT_CACHE_TIMEOUT,TimeUnit.SECONDS);   //读取数据的话，做一个读延期，每天来访问的商品 可以使作为热数据 长时间放在缓存中
                                                                                        //EXPIRE key seconds 命令多少秒后过期，ttl key 剩余多少时间，persist key 移除过期时间，一直存在
            return product;
        }


        //没有的话，从数据库里面拿一份
        product=productDao.get(productId);
        if(product != null ) {  //不为空的话 ，从数据库里面拿一份
            redisUtil.set(productCacheKey,JSON.toJSONString(product));
        }

        return product;
    }

    //升级版， 应对缓存穿透场景，或者说应对Ddos攻击，负载均衡需要设计限流，这里只讨论缓存穿透，可以返回空值，控制使用常量EMPTY_CACHE
    public Product get02(Long productId) {
        Product product = null;

        // RedisKeyPrefixConst.PRODUCT_CACHE的目的是为了保持前缀相同
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

        //从redis缓存中把商品拿出来
        String productStr = redisUtil.get(productCacheKey);
        if (!StringUtils.isEmpty(productStr)) {  // 判断字符串是不是为空
            if (EMPTY_CACHE.equals(productStr)){     //返回到前端
                //同样也加一个过期时间
                redisUtil.expire(productCacheKey,genEmptyCacheTimeout(),TimeUnit.SECONDS);
                return null;
            }
            product=JSON.parseObject(productStr,Product.class);         //转换成商品直接返回
            redisUtil.expire(productCacheKey,genProductCacheTimeout(),TimeUnit.SECONDS);   //读取数据的话，做一个读延期，每天来访问的商品 可以使作为热数据 长时间放在缓存中
            //EXPIRE key seconds 命令多少秒后过期，ttl key 剩余多少时间，persist key 移除过期时间，一直存在
            return product;
        }


        //没有的话，从数据库里面拿一份
        product=productDao.get(productId);
        if(product != null ) {  //不为空的话 ，从数据库里面拿一份
            redisUtil.set(productCacheKey,JSON.toJSONString(product),genProductCacheTimeout(),TimeUnit.SECONDS);
        } else {
            //设置一个null ，并设置一个时间，所有进来的key都要加 过期时间 ,避免大量的key 占用空间
            //redisUtil.set(productCacheKey,EMPTY_CACHE);
            redisUtil.set(productCacheKey,EMPTY_CACHE,genEmptyCacheTimeout(),TimeUnit.SECONDS);
        }

        return product;
    }



    //版本3 ，应对 冷数据，忽然大量访问的问题 ，使用dcl ，redis查两遍
    public Product get03(Long productId) {
        Product product = null;

        // RedisKeyPrefixConst.PRODUCT_CACHE的目的是为了保持前缀相同
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

        //从redis缓存中把商品拿出来
        String productStr = redisUtil.get(productCacheKey);
        if (!StringUtils.isEmpty(productStr)) {  // 判断字符串是不是为空
            if (EMPTY_CACHE.equals(productStr)){     //返回到前端
                //同样也加一个过期时间
                redisUtil.expire(productCacheKey,genEmptyCacheTimeout(),TimeUnit.SECONDS);
                return null;
            }
            product=JSON.parseObject(productStr,Product.class);         //转换成商品直接返回
            redisUtil.expire(productCacheKey,genProductCacheTimeout(),TimeUnit.SECONDS);   //读取数据的话，做一个读延期，每天来访问的商品 可以使作为热数据 长时间放在缓存中
            //EXPIRE key seconds 命令多少秒后过期，ttl key 剩余多少时间，persist key 移除过期时间，一直存在
            return product;
        }

        // 这里将02版本的代码 中的 从mysql捞数据的代码放到 synchronized 锁中，第一次进来的代码会从redis中读，没有，从mysql中拿到数据写到reids中，由于加了synchronized ，其他线程得等着，再次读就有了
        synchronized (this){   // 但是这里使用this 将所有商品都阻塞了，下面用分布式锁来

            //上面的代码拿来再查一次
            //从redis缓存中把商品拿出来
            productStr = redisUtil.get(productCacheKey);
            if (!StringUtils.isEmpty(productStr)) {  // 判断字符串是不是为空
                if (EMPTY_CACHE.equals(productStr)){     //返回到前端
                    //同样也加一个过期时间
                    redisUtil.expire(productCacheKey,genEmptyCacheTimeout(),TimeUnit.SECONDS);
                    return null;
                }
                product=JSON.parseObject(productStr,Product.class);         //转换成商品直接返回
                redisUtil.expire(productCacheKey,genProductCacheTimeout(),TimeUnit.SECONDS);   //读取数据的话，做一个读延期，每天来访问的商品 可以使作为热数据 长时间放在缓存中
                //EXPIRE key seconds 命令多少秒后过期，ttl key 剩余多少时间，persist key 移除过期时间，一直存在
                return product;
            }


            //没有的话，从数据库里面拿一份
            product=productDao.get(productId);
            if(product != null ) {  //不为空的话 ，从数据库里面拿一份
                redisUtil.set(productCacheKey,JSON.toJSONString(product),genProductCacheTimeout(),TimeUnit.SECONDS);
            } else {
                //设置一个null ，并设置一个时间，所有进来的key都要加 过期时间 ,避免大量的key 占用空间
                //redisUtil.set(productCacheKey,EMPTY_CACHE);
                redisUtil.set(productCacheKey,EMPTY_CACHE,genEmptyCacheTimeout(),TimeUnit.SECONDS);
            }
        }

        return product;
    }



    //升级  分布式锁 ，双重查询，查redis ，加锁，查redis, 查mysql
    public Product get03(Long productId) throws InterruptedException {
        Product product = null;
        String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;   //拿取前缀

        product = getProductFromCache(productCacheKey);
        if (product != null) {    //拿出来不为空 直接返回
            return product;
        }

        //DCL ，针对同一个商品id 加分布式锁 ，解决热点缓存并发重建问题
        RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
        hotCacheLock.lock();

        //下面这一行是将串行转并发，预计第一次插入很快的话，可以在写入数据后，使用tryLock ，解开锁 ，增加并发 ，但是3秒内这第一个数据没写完，那么bug 直接将数据库打穿 ，线上要谨慎
        //boolean result = hotCacheLock.tryLock(3, TimeUnit.SECONDS);
        try {
            product = getProductFromCache(productCacheKey);
            if (product != null) {
                return product;
            }

            //下面两行是第一版缓存和数据库数据不一致的解决 ；注意查数据库，写缓存要加锁，上面的更新也要加分布式锁
            RLock updateProductLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
            updateProductLock.lock();


            // 线上都是读多写少，比好价格后才下单，浏览的时间占大部分。 这里是又将锁分成了 读锁上面update的时候用 和写锁 这里用  ；源码就是判断mode ：write就是等待 read就并发
//            RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
//            RLock rLock = readWriteLock.readLock();
//            rLock.lock();
            try {               //没有的话，往数据里面拿一份数据
                product = productDao.get(productId);

                //这里可能有双写不一致的问题 ，解决，让上一个先处理，处理好了再搞下一个



                if (product != null) {
                    redisUtil.set(productCacheKey, JSON.toJSONString(product),
                            genProductCacheTimeout(), TimeUnit.SECONDS);

                    //这一行是二级缓存 ，不同缓存的 同一个key
                    productMap.put(productCacheKey, product);
                } else {
                    redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                }
            } finally {
                updateProductLock.unlock();
//               rLock.unlock();
            }
        } finally {
            hotCacheLock.unlock();
        }
        return product;
    }






    /*
    * 下面是 分支代码
    * */


    private Integer genProductCacheTimeout() {

        //随机时间
        return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
    }

    private Integer genEmptyCacheTimeout() {

        return 60 + new Random().nextInt(30);
    }

    // 重复的 redis里面查数据，提取出来方法
    private Product getProductFromCache(String productCacheKey) {

        //下面四行代码 二级缓存
        Product product = productMap.get(productCacheKey);
        if (product != null) {
            return product;
        }

        String productStr = redisUtil.get(productCacheKey);
        if (!StringUtils.isEmpty(productStr)) {
            if (EMPTY_CACHE.equals(productStr)) {
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
//                return null;
                return new Product();   //null 表示没有，null表示是null的产品，需要区分下
            }
            product = JSON.parseObject(productStr, Product.class);
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS); //读延期
        }
        return product;
    }

}